﻿using System;
using BillsDomain;
using System.ComponentModel;
using System.Globalization;
using System.Xml.Serialization;
using System.IO;

namespace BillsPresentation
{
    /// <summary>
    /// Business logic for the payment reminder view
    /// </summary>
    public class PaymentReminderPresenter
    {
        /// <summary>
        /// Gets or sets the controller.
        /// </summary>
        /// <value>The controller.</value>
        public IScheduledPaymentController Controller { get; set; }

        /// <summary>
        /// The view
        /// </summary>
        private readonly IPaymentReminderView view;

        /// <summary>
        /// The list of all days and their balances for the graph on the view
        /// </summary>
        private readonly BindingList<CalendarDay> allDaysInARangeForGraph = new BindingList<CalendarDay>();

        /// <summary>
        /// The list of days with payments for the monthly calendar on the view
        /// </summary>
        private readonly BindingList<CalendarDay> allDaysInARangeForCalendar = new BindingList<CalendarDay>();

        /// <summary>
        /// The list of upcoming payments
        /// </summary>
        private readonly UpcomingPaymentsCollection upcomingPayments = new UpcomingPaymentsCollection();

        /// <summary>
        /// Initializes a new instance of the PaymentReminderPresenter class.
        /// </summary>
        /// <param name="view">The view.</param>
        /// <param name="controller">The controller.</param>
        public PaymentReminderPresenter(IPaymentReminderView view, IScheduledPaymentController controller)
        {
            this.view = view;
            this.Controller = controller;
            this.Controller.LoadData();
            this.SubscribeViewToEvents();
        }

        /// <summary>
        /// Subscribes the view to events.
        /// </summary>
        public void SubscribeViewToEvents()
        {
            this.view.Load += view_Load;
            this.view.AsOfDateChanged += this.OnAsOfDateChanged;
            this.view.AsOfDateChanged += this.OnProjectedBalanceItemsChanged;
            this.view.FormClose += this.OnFormClosing;
            this.view.ScheduledPaymentChanged += this.OnScheduledPaymentChanged;
            this.Controller.ListChanged += this.OnControllerListChanged;
            this.view.DeleteClicked += this.OnDeleteClicked;
            this.view.ProjectedBalanceItemsChanged += this.OnProjectedBalanceItemsChanged;
            this.view.SpecificDateSelected += this.OnSpecificDateSelected;
            this.view.DataExporting += this.OnDataExporting;
            this.view.DataImporting += this.OnDataImporting;
        }

        private void OnDataExporting(object sender, EventArgs e)
        {
            this.Controller.SaveData(this.view.PathToData);
        }

        private void OnDataImporting(object sender, EventArgs e)
        {
            this.Controller.LoadData(this.view.PathToData);
            this.view_Load(sender, e);
        }

        /// <summary>
        /// Called when a specific date is selected.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="BillsPresentation.DateSelectedEventArgs"/> instance containing the event data.</param>
        private void OnSpecificDateSelected(object sender, DateSelectedEventArgs e)
        {
            this.PopulateCalendar();
            int i = 0;
            CalendarDay currentCalendarDay = this.allDaysInARangeForCalendar[i];
            while (e.SelectedDate.Date != currentCalendarDay.Date.Date && i < this.allDaysInARangeForCalendar.Count)
            {
                i++;
                currentCalendarDay = this.allDaysInARangeForCalendar[i] as CalendarDay;
            }

            this.view.PaymentsForADay = currentCalendarDay.Payments;
        }

        /// <summary>
        /// Called when projected balance items changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnProjectedBalanceItemsChanged(object sender, EventArgs e)
        {
            DateTime projectDateAsOfMidnight = new DateTime(this.view.DateForProjection.Year, this.view.DateForProjection.Month, this.view.DateForProjection.Day);
            this.view.ProjectedBalance = String.Empty;
            this.PopulateGraph();
            if (projectDateAsOfMidnight >= this.allDaysInARangeForGraph[0].Date && projectDateAsOfMidnight <= this.allDaysInARangeForGraph[this.allDaysInARangeForGraph.Count - 1].Date)
            {
                TimeSpan span = projectDateAsOfMidnight - this.allDaysInARangeForGraph[0].Date;
                int index = span.Days;
                this.view.ProjectedBalance = this.allDaysInARangeForGraph[index].EndOfDayBalance.ToString(CultureInfo.CurrentCulture);
            }
            else
            {
                TimeSpan span = projectDateAsOfMidnight - this.view.AsOfDay.Date;
                BindingList<CalendarDay> fromAsOfDayUntilProjectionDate = new BindingList<CalendarDay>();
                this.GenerateDayRangeOfPayments(this.view.AsOfDay.Date, span.Days + 1, fromAsOfDayUntilProjectionDate);
                this.view.ProjectedBalance = fromAsOfDayUntilProjectionDate[span.Days].EndOfDayBalance.ToString(CultureInfo.CurrentCulture);
            }

            this.UpdateUpcomingPaymentsBalances();
            this.view.UpcomingPayments = this.upcomingPayments;
        }

        /// <summary>
        /// Handles the DeleteClicked event of the view control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnDeleteClicked(object sender, EventArgs e)
        {
            this.Controller.DeleteCurrentPayment();
        }

        /// <summary>
        /// Handles the ListChanged event of the controller control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnControllerListChanged(object sender, EventArgs e)
        {
            this.LoadViewOnModel();
        }

        /// <summary>
        /// Called when a scheduled payment is changed or added.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="BillsPresentation.ScheduledPaymentChangedEventArgs"/> instance containing the event data.</param>
        private void OnScheduledPaymentChanged(object sender, ScheduledPaymentChangedEventArgs e)
        {
            this.Controller.SetScheduledPayment(e.SelectedPayment.ScheduledPaymentId);
        }

        /// <summary>
        /// Handles the FormClosing event of the view control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnFormClosing(object sender, EventArgs e)
        {
            this.Controller.SaveData();
        }

        /// <summary>
        /// Called when the as of date is changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnAsOfDateChanged(object sender, EventArgs e)
        {
            if (this.view.DateForProjection < this.view.AsOfDay)
            {
                this.view.DateForProjection = this.view.AsOfDay;
            }

            this.LoadViewOnModel();
        }

        /// <summary>
        /// Populates the graph.
        /// </summary>
        private void PopulateGraph()
        {
            this.GenerateDayRangeOfPayments(this.view.AsOfDay, 90, this.allDaysInARangeForGraph);
            this.view.DaysAndBalances = this.allDaysInARangeForGraph;
#if DEBUG
            XmlSerializer scheduledPaymentsSerializer = new XmlSerializer(typeof(BindingList<CalendarDay>));
            using (TextWriter fileForWriting = new StreamWriter("cal.xml"))
            {
                scheduledPaymentsSerializer.Serialize(fileForWriting, this.allDaysInARangeForGraph);
            }
#endif
        }

        /// <summary>
        /// Generates the upcoming payments.
        /// </summary>
        private void GenerateUpcomingPayments()
        {
            this.upcomingPayments.Clear();
            foreach (ScheduledPayment scheduledPayment in this.Controller.ScheduledPayments)
            {
                if (scheduledPayment.PaymentRecurrence == Recurrence.None && scheduledPayment.FirstDate < this.view.AsOfDay)
                {
                    continue;
                }

                this.upcomingPayments.Add(new PaymentWithDate()
                {
                    Amount = scheduledPayment.Amount * (scheduledPayment.IsDebit ? -1 : 1),
                    IsDebit = String.CompareOrdinal(scheduledPayment.PaymentType.Name, PaymentType.Debit.Name) == 0,
                    Payee = scheduledPayment.Payee,
                    ScheduledPaymentId = scheduledPayment.Id,
                    PaymentDate = CalculateNextPaymentDate(this.view.AsOfDay, scheduledPayment),
                    IncludeInProjection = scheduledPayment.IncludeInProjection
                });

            }

            this.upcomingPayments.Sort();
            this.UpdateUpcomingPaymentsBalances();
        }

        /// <summary>
        /// Updates the upcoming payments balances.
        /// </summary>
        private void UpdateUpcomingPaymentsBalances()
        {
            decimal currentBalance = this.view.CurrentBalance ?? 0;
            int i = 0;
            foreach (PaymentWithDate paymentOnDate in this.upcomingPayments)
            {
                if (paymentOnDate.IncludeInProjection)
                {
                    currentBalance = currentBalance + paymentOnDate.Amount;
                }

                paymentOnDate.Balance = currentBalance;
                this.upcomingPayments.ResetItem(i);
                i++;
            }
        }

        /// <summary>
        /// Generates the day range of payments.
        /// </summary>
        /// <param name="startDate">The start date.</param>
        /// <param name="numberOfDaysToProject">The number of days to project.</param>
        /// <param name="output">The output.</param>
        private void GenerateDayRangeOfPayments(DateTime startDate, int numberOfDaysToProject, BindingList<CalendarDay> output)
        {
            output.Clear();
            decimal currentBalance = this.view.CurrentBalance ?? 0;
            for (int i = 0; i < numberOfDaysToProject; i++)
            {
                DateTime currentDate = startDate.AddDays(i).Date;
                CalendarDay currentCalendarDay = new CalendarDay() { Date = currentDate };
                output.Add(currentCalendarDay);
                currentCalendarDay.EndOfDayBalance = currentBalance;
                foreach (ScheduledPayment scheduledPayment in this.Controller.ScheduledPayments)
                {
                    if (scheduledPayment.PaymentRecurrence == Recurrence.None && scheduledPayment.FirstDate != currentDate)
                    {
                        continue;
                    }

                    DateTime nextPaymentDate = CalculateNextPaymentDate(currentDate, scheduledPayment);
                    if (nextPaymentDate == currentDate)
                    {
                        currentCalendarDay.Payments.Add(new Payment()
                        {
                            Amount = scheduledPayment.Amount * (scheduledPayment.IsDebit ? -1 : 1),
                            IsDebit = String.CompareOrdinal(scheduledPayment.PaymentType.Name, PaymentType.Debit.Name) == 0,
                            Payee = scheduledPayment.Payee,
                            ScheduledPaymentId = scheduledPayment.Id,
                            IncludeInProjection = scheduledPayment.IncludeInProjection
                        });

                        if (scheduledPayment.IncludeInProjection)
                        {
                            if (scheduledPayment.IsDebit)
                            {
                                currentCalendarDay.EndOfDayBalance -= scheduledPayment.Amount;
                            }
                            else
                            {
                                currentCalendarDay.EndOfDayBalance += scheduledPayment.Amount;
                            }
                        }
                    }
                }

                currentCalendarDay.Payments.Sort();
                currentBalance = currentCalendarDay.EndOfDayBalance;
            }
        }

        /// <summary>
        /// Populates the calendar.
        /// </summary>
        private void PopulateCalendar()
        {
            this.GenerateDayRangeOfPayments(this.view.DayOnCalendar.AddMonths(-2), 180, this.allDaysInARangeForCalendar);
            BindingList<CalendarDay> datesWithPayments = new BindingList<CalendarDay>();
            foreach (CalendarDay item in allDaysInARangeForCalendar)
            {
                if (item.Payments.Count > 0)
                {
                    datesWithPayments.Add(item);
                }
            }

            this.view.DatesWithPayments = datesWithPayments;
        }

        /// <summary>
        /// Loads the view on model.
        /// </summary>
        private void LoadViewOnModel()
        {
            this.GenerateUpcomingPayments();

            this.PopulateGraph();
            this.PopulateCalendar();
            this.view.UpcomingPayments = this.upcomingPayments;
            this.OnSpecificDateSelected(this, new DateSelectedEventArgs() { SelectedDate = this.view.DayOnCalendar });
        }

        /// <summary>
        /// Handles the Load event of the view control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void view_Load(object sender, EventArgs e)
        {
            this.view.AsOfDay = DateTime.Today;
            LoadViewOnModel();
        }

        /// <summary>
        /// Calculates the next payment date.
        /// </summary>
        /// <param name="asOfDate">As of date.</param>
        /// <param name="scheduledPayment">The scheduled payment.</param>
        /// <returns></returns>
        private static DateTime CalculateNextPaymentDate(DateTime asOfDate, ScheduledPayment scheduledPayment)
        {
            DateTime result = scheduledPayment.FirstDate;
            DateTime asOfDateAtMidnight = new DateTime(asOfDate.Year, asOfDate.Month, asOfDate.Day);
            if (scheduledPayment.PaymentRecurrence == Recurrence.Biweekly)
            {
                while (result < asOfDateAtMidnight)
                {
                    result = result.AddDays(14);
                }
            }
            else if (scheduledPayment.PaymentRecurrence == Recurrence.Bimonthly)
            {
                while (result < asOfDateAtMidnight)
                {
                    result = result.AddMonths(2);
                }
            }
            else if (scheduledPayment.PaymentRecurrence == Recurrence.Monthly)
            {
                while (result < asOfDateAtMidnight)
                {
                    result = result.AddMonths(1);
                }
            }
            else if (scheduledPayment.PaymentRecurrence == Recurrence.Weekly)
            {
                while (result < asOfDateAtMidnight)
                {
                    result = result.AddDays(7);
                }
            }

            return result;
        }
    }
}
